package chat;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;

import javax.jms.JMSException;
import javax.jms.MessageListener;
import javax.jms.Session;
import javax.jms.TextMessage;
import javax.jms.Topic;
import javax.jms.TopicConnection;
import javax.jms.TopicConnectionFactory;
import javax.jms.TopicPublisher;
import javax.jms.TopicSession;
import javax.jms.TopicSubscriber;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

import listeners.Control;
import listeners.Translate;

import Logging.Monitor;

public class LingvoChat {

////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
	
	/**
	 * This class is responsible to send "I'm Alive" messages to each group we
	 * participate in.
	 * @author Ron Galay
	 */
	class aliveAction extends TimerTask {

		private String topicName;
		
		// the data is sent to topics named <group>Control
		public aliveAction(String topicName) {
			this.topicName = topicName;
		}
		
		@Override
		public void run() {
			//the I'm alive message is "Alive-" + the user name.
			publishMsq(topicName,"Alive-" + userName);
		}
		
	}
	
	/**
	 * This class is responsible for:
	 * <ol>
	 *  <li>removing a user from a group:
	 * 	<ol>
	 * 		<li>Traversing each group the user is in, and for each group we enter
	 * 			a new entry to personsInGroup without the user.</li>
	 * 		<li>Removing the user entry from userInGroup</li>
	 * 	</ol>
	 * 	</li>
	 * </ol>
	 * 
	 * @author Ron Galay
	 */
	class controlMessage extends TimerTask {

		private final String userName;
		
		public controlMessage(String userName) {
			this.userName = userName;
		}
		
		@Override
		public void run() {
			
			// TODO check if a dead-lock cannot be reached
			synchronized(userInGroups) {
				synchronized(usersInGroup) {
					
					// removing the user from all of his groups
					for(String group: userInGroups.get(userName)) {
						usersInGroup.get(group).remove(userName);
					}
					// removing the user entry from userInGroup
					userInGroups.remove(userName);
				}
			}
		}
		
	}
	
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
	
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
	
	// for the communication with rest of the groups
	Context jndiContext = null;
	TopicConnectionFactory topicConnectionFactory = null;
	TopicConnection topicConnection = null;
	TopicSession topicSession = null;
	MessageListener controlListener;
	
	// all access to the maps should be guarded by synchronized
	private Collection<TopicPublisher> 
			publishToGroups = 
			new LinkedList<TopicPublisher>();
	private Map<String,Timer>
			aliveToGroup = 
			new HashMap<String,Timer>();
	private Map<String,Collection<chatUser>> 
			usersInGroup =
			new HashMap<String,Collection<chatUser>>();
	private Map<String,TopicSubscriber> //group,topic
			groupTopicSubscibers =
			new HashMap<String,TopicSubscriber>();
	private Map<chatUser,Collection<String>> 
			userInGroups = 
			new HashMap<chatUser,Collection<String>>();
	
	
	// user specific info
	private final String userName;
	private final String lang;
	private final static LinkedList<String> 
				allowedLang = 
				new LinkedList<String>(
						Arrays.asList(new String[]{"ENGLISH","SPANISH","FRENCH","GERMAN"})
					);
	
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
	
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
	
	public LingvoChat(String username,String lang) {
		
		this.userName = username;
		this.lang = lang;
		
	}
	
	private void initMessaging() {
		
		try {
			jndiContext = new InitialContext();
			topicConnectionFactory = (TopicConnectionFactory) 
									jndiContext.lookup("TopicConnectionFactory");
			
			topicConnection = topicConnectionFactory.createTopicConnection();
			topicSession = topicConnection.createTopicSession(false, 
															  Session.AUTO_ACKNOWLEDGE);
			
		} catch (NamingException e) {
			// TODO Auto-generated catch block
			quit();
		} catch (JMSException e) {
			// TODO Auto-generated catch block
			quit();
		}
	}
	
	private void initListner(String control,MessageListener topicListener) {
		
		try {
			Topic topic = (Topic) jndiContext.lookup(control);
			TopicSubscriber topicSubscriber = topicSession.createSubscriber(topic);
			topicSubscriber.setMessageListener(topicListener); 
			//todo - save the topic subscriber to map - group to subscribers
			topicConnection.start();
			synchronized (groupTopicSubscibers) {
				groupTopicSubscibers.put(control, topicSubscriber);
			}
			
		} catch (NamingException e) {
			// TODO Auto-generated catch block
			quit();
		} catch (JMSException e) {
			// TODO Auto-generated catch block
			quit();
		} 
		
	}
	
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
	
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
	
	private void publishMsq(String topicName,String message) {
		
		try {
			Topic topic = (Topic) jndiContext.lookup(topicName);
			TopicPublisher topicPublisher = topicSession.createPublisher(topic);
			TextMessage textMessage = topicSession.createTextMessage();
			textMessage = topicSession.createTextMessage(message);
			topicPublisher.publish(textMessage);
		} catch (NamingException e) {
			// TODO Auto-generated catch block
			quit();
		} catch (JMSException e) {
			// TODO Auto-generated catch block
			quit();
		}

	}
	
	private void join(String group) {
		
		Monitor.write(null, "Handling a join request for group:"+group);
		if( !(userInGroups.containsKey(userName) &&
			userInGroups.get(userName).contains(group)) ) {
			
			// sending the joined in message - "Joined-<username>"
			String topicName = group+"Control";
			publishMsq(topicName, "Joined-" + userName);
			// receiving the "ack" messages from the participants of the group
			initListner(topicName, new Control(group));
			// giving time for the "ack" messages to arrive - 30 seconds
			try {
				// TODO we might to busywait instead
				Thread.sleep(30000);
			} catch (InterruptedException ignore) {}
			
			// starting off the "I'm Alive" messaging"
			Timer controlTimer = new Timer();
			controlTimer.scheduleAtFixedRate(
					new aliveAction(topicName),
					System.currentTimeMillis(),
					10000);
			Monitor.write(this, "Started off the control messaging");
			synchronized (aliveToGroup) {
				aliveToGroup.put(topicName, controlTimer);
			}
			
			// initializing the receiving of messages from the group
			initListner(group, new Translate(lang));
		}
		
		// printing the participants of the group
		groupStatus(group);
		
	}

	private void leave(String group) {
		
		Monitor.write(null, "Handling a leave request for  group:"+group);
	}
	
	private void publish(String group,String message) {
		
		Monitor.write(null, "Handling a publish request for group:"+group+
							" with the message:"+message);
	}
	
	private void groupStatus(String groupName) {
		
		Monitor.write(null, "Handling a groupStatuses request");
		synchronized (usersInGroup) {
			for(chatUser user : usersInGroup.get(groupName) ){
				System.out.println("User:"+user.getName()+
									" Speaks:"+user.getLang());
			}
		}
	}
	
	private void myGroups() {
		
		Monitor.write(null, "Handlinng a myGroups request");
		// TODO check if no dead lock can be reached
		synchronized (userInGroups) {
			for (String group : userInGroups.get(userName)) {
				groupStatus(group);
			}
		}
	}
	
	private void quit() {
		
		Monitor.write(null, "Handling a quit request");
	}
	
	
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
	
	
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
	
	private boolean exeCommand(String commandLine) {
		
		// parse and find what command should we execute
		int commandEnding = commandLine.indexOf(" ");
		commandEnding =	(commandEnding == -1)?
						commandLine.length():
						commandEnding;
		// in upper case to make our comparison easier
		String command = commandLine.substring(0, commandEnding).
									 toUpperCase(); 
		String parameter = (commandEnding == commandLine.length())?
							command:
							commandLine.substring(commandEnding+1);
		
		if(command.equals("JOIN")) {
			
			join(parameter);
			
		} else if(command.equals("LEAVE")) {
			
			leave(parameter);
			
		} else if(command.equals("PUBLISHMSG")) {
			
			int paramDelim = parameter.indexOf(" ");
			publish(parameter.substring(0,paramDelim),
					parameter.substring(paramDelim+1));
			
		} else if(command.equals("GROUPSTATUS")) {
			
			groupStatus(parameter);
			
		} else if(command.equals("MYGROUPS")) {
			
			myGroups();
			
		} else if (command.equals("QUIT")) {
			
			quit();
			return false;
			
		}
		return true;
	}
	
	public static void main(String[] args) throws IOException {
		 
		
		// check that the parameters entered in the correct format
		if( args.length != 2) {
			System.out.println("Please insert parameters in the correct format:\n" +
			"<username> {English | Spanish | French | German}");
			System.exit(0);
		} 
		
		String enteredUsername = args[0];
		// checking that the language is one of {English,Spanish,French,German}
		if(!allowedLang.contains(args[1].toUpperCase())) {
			System.out.println("Please insert a language to be one of the following:\n" +
								"{English,Spanish,French,German}");
			System.exit(0);
		}
		String enteredLang = args[1];
		Monitor.write(null, "Recieved the following parameters:\n" +
							"username:"+enteredUsername+"\n"+
							"langauage:"+enteredLang);
		
		LingvoChat userInstance = new LingvoChat(enteredUsername,enteredLang);
		userInstance.initMessaging();
		
		// waiting for instructions from the user
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		while(userInstance.exeCommand(br.readLine()));
	}
	
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
	
}
